iT邦幫忙

2022 iThome 鐵人賽

DAY 4
0
Software Development

《今天也走在開發遊戲引擎的路上》系列 第 4

「遊戲引擎系統組件」 —— 低階引擎系統 (一)

  • 分享至 

  • xImage
  •  

前言

接下來就是準備要陸續進入實作環節的部份了。筆者曾在網上一篇文章看過,軟體開發有幾種模式,傳統上以完成整體設計,然後根據細節設計、實現。就好像一個已經技術成熟、胸有成竹的工匠可以穩定的拿著設計圖就直接施工,然而這是一個擁有豐富經驗的工匠可以採取的做法。筆者畢竟是學習者,更偏向另外一種以探索為主的開發方法,不斷試錯、不斷摸索道路。

雖然是這麼說,不過我仍然想在開始之前先來做一個小小的規劃。接下來我會先盤點遊戲引擎系統組件一些較為核心的功能,不只作為之後實作時會著重的地方,也是為將要摸索的道路先做上一些記號,如此一來雖然是以嘗試代替計畫,卻仍然不會偏離原來所設想。


子系統管理器

遊戲引擎是一個複雜的軟體架構,就像我們先前介紹的有許多子系統。不同的子系統間會有依賴關係,也因此對於不同子系統間啟動的順序就被隱式的決定了。

子系統的啟動與終止

首先我們會先想到的是C++的建構函式與解構函式

class RenderManager
{
public:
    RenderManager()    {}
    ~RenderManager()   {}
};

static RenderManager gRenderManager;

然而仔細想想後,會發現這是行不通的。我們在先前就提到,子系統是有依賴關係的,而C++中的全域或是靜態實例會在進入main()之前被建構,而他們所調用的順序是無法決定的。因此建構與解構函式是不適合來對子系統初始化的。

而這個問題其實有個簡單又快速的解決方法。

class RenderManager
{
public:
    RenderManager()    {}
    ~RenderManager()   {}
    
    void startUp()     {/* 啟動管理器 */}
    void shutDown()    {/* 終止管理器 */}
};

RenderManager gRenderManager;

int main(int argc,char* argv)
{
    gRenderManager.startUp();
    // 運行遊戲
    gRenderManager.shutDown();
}

雖然還有其他的實現方法,但是筆者認為這種方法既簡單又明確,在摸索之初,以這種簡單粗暴的方法總是在調試或是維護時更能顯現出錯誤點。


遊戲循環

遊戲由許多子系統所構成,包含輸入、輸出、渲染、動畫、物理、AI等等。而在遊戲運行時通常都會有週期性的更新,或是30Hz又或是60Hz,在物理動力學模擬的部分可能還需要更高更新頻率、而AI的Brain則或許只要每秒1、2次的更新就好了。而這種使各個引擎子系統週期性更新的循環,也被稱為遊戲循環(Game Loop)。

循環框架

這裡下面也稍微介紹一下常見的循環框架。

  • 消息泵
    在一些作業系統上面,除了服務遊戲、遊戲引擎本體以外,還要處理來自作業系統的訊息。因此Windows上的遊戲都會有一段消息泵(Message pump),在循環中,會先將來自Windows的消息處理完後才繼續執行引擎的循環。
while(true)
{
    MSG msg;
    while(PeekMessage(&msg,NULL,0,0) > 0)
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    RunOneIterationOFGameLoop();
}
  • 回調驅動框架
    在設計遊戲引擎中,常見使用Library的方式來架構。基本上會提供使用者Function或是Class,這種架構雖然提供了大限度的自由,但是學習使用一個庫建構遊戲的成本卻相對地高。在另外一種方式上,是使用框架(Framework)來架構。框架是一個未完成的軟體架構,使用框架透過完成其缺少的自定義實現又或是複寫。不過對於其流程上面就少了一些控制上的自由度了。

以下使用代碼來舉例。

class frameListener
{
public:
    virtual void frameStarted()
    {
        //渲染之前所需要執行的事情
    }
    
    virtual void frameEnded()
    {
        //渲染之後所需要執行的事情
    }
};

while(true)
{
    for(auto &frameListener:frameListener)
    {
        frameListener.frameStarted();
    }
    renderCurremtScene();
    for(auto &frameListener:frameListener)
    {
        frameListener.frameEnded();
    }
    finalizeSceneAndSwapBuffers();
}

受CPU影響的循環

在早期的遊戲,遊戲循環中並沒有與真實時間有關聯,就像上面幾個框架中,循環會在執行結束後直接開始下一次的循環。這也會導致遊戲在不同的CPU影響而有著不同的執行速度。

這裡是一個小例子,這是筆者之前訓練貪吃蛇DQN的小專題,為了測試成效用了C++來刻一個可視化的環境。可以清楚的看見,上圖是筆者換了新電腦後運行的情況,而下圖則是舊筆電的執行狀況,可以明顯地看見,遊戲並未更改,然而上圖就像是快進了一樣。

而我們也能實現透過一個「時鐘」的概念來完成它。

// 設定一個理想的一幀的時間
F32 dtSeconds = 1.0f / 30.0f;
// 先讀取當前時間
U64 tBegin = readHiResTimer();

while(true)
{
    runOneIterationOfGameLoop(dtSeconds);
    // 讀取當前時間,計算增量
    U64 tEnd = readHiResTimer();
    dtSeconds = (tEnd - tBegin)/ getHiRestTimerFrequency();
    // tEnd為下一幀的tBegin
    tBegin = tend;
}

而對於遊戲循環的方法還有許多種,筆者雖然想一個一個慢慢介紹,但礙於時間緣故就不再此多做介紹了。感興趣的朋友們可以看看這篇文章,而在之後的實作環節筆者必定會將其都整理進去的!


後記

今天是學校的社團迎新茶會,忙來忙去因此10.才開始寫今天的鐵人。
/images/emoticon/emoticon02.gif
其實在筆記中還記上了裝置輸入輸出、調試及日誌工具、記憶體管理、容器 (Containner)、文件系統這幾個想要介紹的點! 但由於時間不早了,剩下的內容將在明天繼續補齊,雖然時間表可能會因此稍微亂掉,不過遊戲引擎本身就是一個龐大的架構,筆者本來就預計會寫超過30天! 那就先這樣吧,日常勉勵一下自己。
/images/emoticon/emoticon06.gif


上一篇
「深其根、固其柢」 —— 從架構開始扎穩基礎 (二)
下一篇
「遊戲引擎系統組件」 —— 記憶體管理系統
系列文
《今天也走在開發遊戲引擎的路上》12
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言